package com.hzya.frame;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.hzya.frame.dto.ResultDataDto;
import com.hzya.frame.dto.ResultDto;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.UpdateOptions;
import com.mongodb.client.model.Updates;
import okhttp3.*;
import org.apache.commons.io.IOUtils;
import org.apache.nifi.annotation.behavior.InputRequirement;
import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
import org.apache.nifi.annotation.behavior.WritesAttribute;
import org.apache.nifi.annotation.behavior.WritesAttributes;
import org.apache.nifi.annotation.documentation.CapabilityDescription;
import org.apache.nifi.annotation.documentation.Tags;
import org.apache.nifi.annotation.lifecycle.OnScheduled;
import org.apache.nifi.annotation.lifecycle.OnStopped;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.expression.ExpressionLanguageScope;
import org.apache.nifi.flowfile.FlowFile;
import org.apache.nifi.logging.ComponentLog;
import org.apache.nifi.processor.*;
import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.processor.util.StandardValidators;
import org.bson.Document;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;

@Tags({"吉客云", "电商", "ERP", "http", "api", "hzya", "mongodb"})
@CapabilityDescription("从吉客云开放平台分页拉取数据。处理器使用外部MongoDB管理时间窗口和分页状态，实现自动化的全量及增量同步，并内置了签名逻辑。")
//@InputRequirement(Requirement.INPUT_FORBIDDEN)//禁止输入
//@InputRequirement(Requirement.INPUT_REQUIRED)//必须输入
@InputRequirement(Requirement.INPUT_ALLOWED)//允许输入
@WritesAttributes({@WritesAttribute(attribute = "jackyun.api.method", description = "本次调用的API方法名"), @WritesAttribute(attribute = "jackyun.api.startTime", description = "本次API调用的查询开始时间"), @WritesAttribute(attribute = "jackyun.api.endTime", description = "本次API调用的查询结束时间"), @WritesAttribute(attribute = "jackyun.api.pageIndex", description = "本次API调用的数据页码")})
public class GetJackyunOpenDataProcessor extends AbstractProcessor {

    // --- 吉客云相关属性 ---
    public static final PropertyDescriptor PROP_GATEWAY_URL = new PropertyDescriptor.Builder().name("吉客云网关URL").description("吉客云开放平台的网关地址。").defaultValue("https://open.jackyun.com/open/openapi/do").required(true).addValidator(StandardValidators.URL_VALIDATOR).build();
    public static final PropertyDescriptor PROP_APP_KEY = new PropertyDescriptor.Builder().name("App Key").description("在吉客云开放平台上申请的AppKey。").required(true).addValidator(StandardValidators.NON_EMPTY_VALIDATOR).build();
    public static final PropertyDescriptor PROP_APP_SECRET = new PropertyDescriptor.Builder().name("App Secret").description("在吉客云开放平台上申请的AppSecret。").required(true).sensitive(true).addValidator(StandardValidators.NON_EMPTY_VALIDATOR).build();
    public static final PropertyDescriptor PROP_API_METHOD = new PropertyDescriptor.Builder().name("API方法名").description("要调用的吉客云API方法名，例如：erp.storage.goodsdocout.v2").required(true).expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES).addValidator(StandardValidators.NON_EMPTY_VALIDATOR).build();
    public static final PropertyDescriptor PROP_API_VERSION = new PropertyDescriptor.Builder().name("API版本号").description("要调用的吉客云API版本号，例如：v1.0").defaultValue("v1.0").required(true).addValidator(StandardValidators.NON_EMPTY_VALIDATOR).build();

    // --- 数据拉取逻辑相关属性 ---
    public static final PropertyDescriptor PROP_INITIAL_START_TIME = new PropertyDescriptor.Builder().name("初始同步开始时间").description("首次运行时使用的查询开始时间。格式必须为 'yyyy-MM-dd HH:mm:ss'。这是定义全量同步起点的关键。").required(true).addValidator(StandardValidators.NON_EMPTY_VALIDATOR).build();
    public static final PropertyDescriptor PROP_TIME_WINDOW_INCREMENT = new PropertyDescriptor.Builder().name("时间窗口增量").description("处理器内部循环调用API时，每个时间窗口的最大时长，用于遵守API的查询范围限制。").defaultValue("59 minutes").required(true).addValidator(StandardValidators.TIME_PERIOD_VALIDATOR).build();
    public static final PropertyDescriptor PROP_END_TIME_SAFETY_BUFFER = new PropertyDescriptor.Builder().name("结束时间安全缓冲").description("从当前时间减去的缓冲量，以防止因服务器时钟不一致而查询未来时间。").defaultValue("5 minutes").required(true).addValidator(StandardValidators.TIME_PERIOD_VALIDATOR).build();
    public static final PropertyDescriptor PROP_PAGE_SIZE = new PropertyDescriptor.Builder().name("每页数据条数").description("每次API请求期望返回的数据条数。").defaultValue("100").required(true).addValidator(StandardValidators.INTEGER_VALIDATOR).build();
    public static final PropertyDescriptor PROP_REQUEST_BIZ_CONTENT = new PropertyDescriptor.Builder().name("业务请求参数 (BizContent)").description("API的业务请求参数(JSON格式)。处理器会自动注入分页(pageIndex, pageSize)和时间(gmtModifiedStart, gmtModifiedEnd)参数，无需在此填写。").required(true).addValidator(StandardValidators.NON_EMPTY_VALIDATOR).build();
    public static final PropertyDescriptor PROP_START_TIME_PARAM_NAME = new PropertyDescriptor.Builder().name("开始时间参数名").description("在BizContent中用于传递查询开始时间的字段名。").defaultValue("gmtModifiedStart").required(true).addValidator(StandardValidators.NON_EMPTY_VALIDATOR).build();
    public static final PropertyDescriptor PROP_END_TIME_PARAM_NAME = new PropertyDescriptor.Builder().name("结束时间参数名").description("在BizContent中用于传递查询结束时间的字段名。").defaultValue("gmtModifiedEnd").required(true).addValidator(StandardValidators.NON_EMPTY_VALIDATOR).build();
    public static final PropertyDescriptor PROP_PAGINATION_ENABLED = new PropertyDescriptor.Builder().name("是否启用分页 (Pagination Enabled)").displayName("是否启用分页").description("对于某些不遵循分页规则的API（例如总是返回全量数据），请将此项设为false，处理器将只请求第一页数据。").required(true).allowableValues("true", "false").defaultValue("true").build();

    // --- MongoDB 相关属性 ---
    public static final PropertyDescriptor PROP_MONGO_URI = new PropertyDescriptor.Builder().name("MongoDB Connection String").description("MongoDB的连接URI，例如：mongodb://user:password@host1:27017,host2:27017").required(true).addValidator(StandardValidators.NON_EMPTY_VALIDATOR).build();
    public static final PropertyDescriptor PROP_MONGO_DATABASE = new PropertyDescriptor.Builder().name("MongoDB Database Name").description("用于存储状态的数据库名称。").required(true).addValidator(StandardValidators.NON_EMPTY_VALIDATOR).build();
    public static final PropertyDescriptor PROP_MONGO_COLLECTION = new PropertyDescriptor.Builder().name("MongoDB Collection Name").description("用于存储状态的集合名称。").defaultValue("nifi_processor_states").required(true).addValidator(StandardValidators.NON_EMPTY_VALIDATOR).build();
    public static final PropertyDescriptor PROP_STATE_KEY_NAME = new PropertyDescriptor.Builder().name("状态存储键名 (State Key)").description("在MongoDB中用于存储此任务状态的唯一键名。例如 '销售结算单同步'。这将作为文档的 _id。").required(true).expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES).addValidator(StandardValidators.NON_EMPTY_VALIDATOR).build();

    // --- 关系定义 (Relationships) ---
    public static final Relationship REL_SUCCESS = new Relationship.Builder().name("成功").description("成功从API获取并解析的数据页将路由到此关系。").build();
    public static final Relationship REL_FAILURE = new Relationship.Builder().name("失败").description("API调用失败或发生不可恢复的错误时，将触发FlowFile路由到此关系。").build();
    public static final Relationship REL_NO_DATA = new Relationship.Builder().name("无数据").description("在检查的时间范围内，API没有返回任何新数据时，将触发FlowFile路由到此关系。").build();

    private List<PropertyDescriptor> descriptors;
    private Set<Relationship> relationships;
    private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    private volatile OkHttpClient httpClient;
    private volatile MongoClient mongoClient;

    @Override
    protected void init(final ProcessorInitializationContext context) {
        final List<PropertyDescriptor> descriptors = new ArrayList<>();
        descriptors.add(PROP_GATEWAY_URL);
        descriptors.add(PROP_APP_KEY);
        descriptors.add(PROP_APP_SECRET);
        descriptors.add(PROP_API_METHOD);
        descriptors.add(PROP_API_VERSION);
        descriptors.add(PROP_INITIAL_START_TIME);
        descriptors.add(PROP_TIME_WINDOW_INCREMENT);
        descriptors.add(PROP_END_TIME_SAFETY_BUFFER);
        descriptors.add(PROP_PAGE_SIZE);
        descriptors.add(PROP_REQUEST_BIZ_CONTENT);
        descriptors.add(PROP_START_TIME_PARAM_NAME);
        descriptors.add(PROP_END_TIME_PARAM_NAME);
        descriptors.add(PROP_PAGINATION_ENABLED);
        // MongoDB属性
        descriptors.add(PROP_MONGO_URI);
        descriptors.add(PROP_MONGO_DATABASE);
        descriptors.add(PROP_MONGO_COLLECTION);
        descriptors.add(PROP_STATE_KEY_NAME);
        this.descriptors = Collections.unmodifiableList(descriptors);

        final Set<Relationship> relationships = new HashSet<>();
        relationships.add(REL_SUCCESS);
        relationships.add(REL_FAILURE);
        relationships.add(REL_NO_DATA);
        this.relationships = Collections.unmodifiableSet(relationships);
    }

    @Override
    public Set<Relationship> getRelationships() {
        return this.relationships;
    }

    @Override
    public final List<PropertyDescriptor> getSupportedPropertyDescriptors() {
        return descriptors;
    }

    @OnScheduled
    public void onScheduled(final ProcessContext context) {
        // 在处理器启动时，创建可复用的HTTP和MongoDB客户端，以利用连接池提高性能
        this.httpClient = new OkHttpClient.Builder().connectTimeout(30, TimeUnit.SECONDS).readTimeout(60, TimeUnit.SECONDS).build();
        this.mongoClient = MongoClients.create(context.getProperty(PROP_MONGO_URI).getValue());
    }

    @OnStopped
    public void onStopped() {
        // 在处理器停止时，安全地关闭客户端连接，释放资源
        if (this.mongoClient != null) {
            this.mongoClient.close();
        }
    }

    protected OkHttpClient getHttpClient() {
        return this.httpClient;
    }

    @Override
    public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException {
        // 1. 尝试获取上游FlowFile。在INPUT_ALLOWED模式下，如果没有输入，flowFile会是null
        FlowFile flowFile = session.get();

        // 注意：stateKey的表达式语言现在可能基于一个可能为null的FlowFile。
        // 如果您在表达式中使用了FlowFile的属性，需要确保它能处理null的情况。
        final String stateKey = context.getProperty(PROP_STATE_KEY_NAME).evaluateAttributeExpressions(flowFile).getValue();
        if (stateKey == null || stateKey.trim().isEmpty()) {
            getLogger().error("状态存储键名(State Key)未配置或为空，无法进行状态管理。");
            // 如果有出错的输入FlowFile，则将其路由到failure
            if (flowFile != null) {
                session.transfer(flowFile, REL_FAILURE);
            }
            context.yield();
            return;
        }

        final MongoDatabase database = mongoClient.getDatabase(context.getProperty(PROP_MONGO_DATABASE).getValue());
        final MongoCollection<Document> stateCollection = database.getCollection(context.getProperty(PROP_MONGO_COLLECTION).getValue());
        final boolean paginationEnabled = context.getProperty(PROP_PAGINATION_ENABLED).asBoolean();

        try {
            // 2. 动态决定bizData模板的来源
            JSONObject bizDataTemplate;
            if (flowFile != null) {
                // 如果有上游FlowFile，尝试读取其内容
                String initialRequestJson;
                try (InputStream in = session.read(flowFile)) {
                    initialRequestJson = IOUtils.toString(in, StandardCharsets.UTF_8);
                }

                if (initialRequestJson != null && !initialRequestJson.trim().isEmpty()) {
                    getLogger().info("检测到上游FlowFile内容，将使用其作为bizData模板。");
                    bizDataTemplate = JSON.parseObject(initialRequestJson);
                } else {
                    getLogger().info("上游FlowFile内容为空，将回退使用处理器属性 '业务请求参数 (BizContent)' 作为模板。");
                    bizDataTemplate = JSON.parseObject(context.getProperty(PROP_REQUEST_BIZ_CONTENT).getValue());
                }
            } else {
                // 如果没有上游FlowFile，则直接使用处理器属性
                getLogger().info("无上游FlowFile，将使用处理器属性 '业务请求参数 (BizContent)' 作为模板。");
                bizDataTemplate = JSON.parseObject(context.getProperty(PROP_REQUEST_BIZ_CONTENT).getValue());
            }

            String startTimeParamName = context.getProperty(PROP_START_TIME_PARAM_NAME).getValue();
            String endTimeParamName = context.getProperty(PROP_END_TIME_PARAM_NAME).getValue();

            // (后续的双模式判断、时间窗口计算等逻辑，完全保持不变)
            final String initialStartTimeValue = context.getProperty(PROP_INITIAL_START_TIME).getValue();
            final boolean syncByTime = initialStartTimeValue != null && !"null".equalsIgnoreCase(initialStartTimeValue.trim());

            long loopStartTimeMillis = 0;
            long catchUpTargetTimeMillis = System.currentTimeMillis();

            if (syncByTime) {
                long safetyBufferMillis = context.getProperty(PROP_END_TIME_SAFETY_BUFFER).asTimePeriod(TimeUnit.MILLISECONDS);
                catchUpTargetTimeMillis = System.currentTimeMillis() - safetyBufferMillis;

                Document stateDoc = stateCollection.find(Filters.eq("_id", stateKey)).first();
                String lastSyncTimeStr = (stateDoc != null) ? stateDoc.getString("value") : null;

                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                if (lastSyncTimeStr == null || lastSyncTimeStr.isEmpty()) {
                    loopStartTimeMillis = sdf.parse(initialStartTimeValue).getTime();
                    getLogger().info("按时间同步模式: 未在MongoDB中找到状态键 '{}' 的记录，将使用初始同步时间: {}", stateKey, sdf.format(new Date(loopStartTimeMillis)));
                } else {
                    loopStartTimeMillis = sdf.parse(lastSyncTimeStr).getTime();
                }

                if (loopStartTimeMillis >= catchUpTargetTimeMillis) {
                    getLogger().info("任务 '{}' 已同步至最新进度(水位线: {}), 无需执行本次任务。", stateKey, sdf.format(new Date(loopStartTimeMillis)));
                    // 如果有输入的flowFile，需要将其处理掉，否则会积压
                    if (flowFile != null) {
                        session.transfer(flowFile, REL_SUCCESS);
                    }
                    context.yield();
                    return;
                }
            } else {
                getLogger().info("单次全量同步模式: 任务 '{}' 将执行一次完整的全量分页拉取。", stateKey);
            }

            long timeWindowMillis = context.getProperty(PROP_TIME_WINDOW_INCREMENT).asTimePeriod(TimeUnit.MILLISECONDS);
            boolean hasFetchedDataInThisRun = false;

            boolean continueLoop;
            do {
                String windowStartTimeStr = null;
                String windowEndTimeStr = null;
                long newWatermarkMillis = 0;

                if (syncByTime) {
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    long nextWindowStartTimeMillis = loopStartTimeMillis + timeWindowMillis;
                    long apiQueryEndTimeMillis;

                    if (nextWindowStartTimeMillis >= catchUpTargetTimeMillis) {
                        apiQueryEndTimeMillis = catchUpTargetTimeMillis;
                        newWatermarkMillis = catchUpTargetTimeMillis;
                    } else {
                        apiQueryEndTimeMillis = nextWindowStartTimeMillis - 1000;
                        newWatermarkMillis = nextWindowStartTimeMillis;
                    }
                    windowStartTimeStr = sdf.format(new Date(loopStartTimeMillis));
                    windowEndTimeStr = sdf.format(new Date(apiQueryEndTimeMillis));
                    getLogger().debug("任务 '{}' 准备处理时间窗口: [{} -> {}]", stateKey, windowStartTimeStr, windowEndTimeStr);
                }

                int currentPage = 0;
                boolean hasNextPageInWindow = true;
                while (hasNextPageInWindow) {
                    JSONObject bizData = (JSONObject) bizDataTemplate.clone();
                    bizData.put("pageIndex", currentPage);
                    bizData.put("pageSize", Integer.valueOf(context.getProperty(PROP_PAGE_SIZE).getValue()));

                    if (flowFile == null) {
                        if (syncByTime) {
                            if (startTimeParamName != null && !startTimeParamName.isEmpty() && !"null".equalsIgnoreCase(startTimeParamName)) {
                                bizData.put(startTimeParamName, windowStartTimeStr);
                            }
                            if (endTimeParamName != null && !endTimeParamName.isEmpty() && !"null".equalsIgnoreCase(endTimeParamName)) {
                                bizData.put(endTimeParamName, windowEndTimeStr);
                            }
                        }
                    }

                    ResultDataDto resultDataDto = callJackyunApi(context, bizData, getLogger());

                    if (resultDataDto == null || !"200".equals(resultDataDto.getCode())) {
                        String errorMsg = (resultDataDto != null) ? resultDataDto.toString() : "API返回为空或格式错误";
                        throw new ProcessException("吉客云API调用失败。 " + errorMsg);
                    }

                    ResultDto result = resultDataDto.getResult();
                    boolean hasDataInPage = (result != null && result.getData() != null && !result.getData().isEmpty());

                    if (hasDataInPage) {
                        hasFetchedDataInThisRun = true;
                        getLogger().info("任务 '{}' 在第 {} 页获取到 {} 条数据。", stateKey, currentPage, result.getData().size());

                        FlowFile ff = (flowFile != null) ? session.create(flowFile) : session.create();
                        ff = session.putAttribute(ff, "jackyun.api.method", context.getProperty(PROP_API_METHOD).evaluateAttributeExpressions(flowFile).getValue());
                        ff = session.putAttribute(ff, "jackyun.api.pageIndex", String.valueOf(currentPage));
                        if (syncByTime) {
                            ff = session.putAttribute(ff, "jackyun.api.startTime", windowStartTimeStr);
                            ff = session.putAttribute(ff, "jackyun.api.endTime", windowEndTimeStr);
                        }

                        ff = session.write(ff, out -> out.write(JSON.toJSONString(result.getData()).getBytes(StandardCharsets.UTF_8)));
                        session.transfer(ff, REL_SUCCESS);
//                        session.commitAsync();
                        currentPage++;

                        if (!paginationEnabled) {
                            getLogger().info("任务 '{}' 的分页功能已被禁用，已处理完单页数据，将停止分页。", stateKey);
                            hasNextPageInWindow = false;
                        }
                    } else {
                        hasNextPageInWindow = false;
                    }
                }

                if (syncByTime) {
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    String newWatermarkStr = sdf.format(new Date(newWatermarkMillis));
                    stateCollection.updateOne(Filters.eq("_id", stateKey), Updates.set("value", newWatermarkStr), new UpdateOptions().upsert(true));
                    getLogger().info("任务 '{}' 的时间窗口 [{} -> {}] 数据同步完成，状态水位线更新至: {}", stateKey, windowStartTimeStr, windowEndTimeStr, newWatermarkStr);
                    loopStartTimeMillis = newWatermarkMillis;
                    continueLoop = loopStartTimeMillis < catchUpTargetTimeMillis;
                } else {
                    continueLoop = false;
                }

            } while (continueLoop);

            // 3. 正确处理原始输入FlowFile的最终去向
            if (flowFile != null) {
                if (hasFetchedDataInThisRun) {
                    // 如果成功拉取到任何数据，则原始的触发FlowFile使命完成，应被移除
                    session.remove(flowFile);
                } else {
                    getLogger().info("任务 '{}' 在本次执行中未发现新数据。", stateKey);
                    // 如果未拉取到任何数据，将原始FlowFile路由到'无数据'关系
                    session.transfer(flowFile, REL_NO_DATA);
                }
            } else if (!hasFetchedDataInThisRun) {
                // 只有在没有输入FlowFile且没有拉取到数据时，才创建一个新的FlowFile发往'无数据'
                getLogger().info("任务 '{}' 在本次执行中未发现新数据。", stateKey);
                FlowFile noDataFlowFile = session.create();
                noDataFlowFile = session.putAttribute(noDataFlowFile, "message", "在指定的时间范围内没有查询到新数据。");
                session.transfer(noDataFlowFile, REL_NO_DATA);
            }

        } catch (Exception e) {
            getLogger().error("处理吉客云数据时发生未知错误: {}", e.getMessage(), e);
            if (flowFile != null) {
                // 如果存在输入的FlowFile，就将它及错误信息路由到failure
                flowFile = session.putAttribute(flowFile, "exception.class", e.getClass().getName());
                flowFile = session.putAttribute(flowFile, "exception.message", e.getMessage());
                session.transfer(flowFile, REL_FAILURE);
            } else {
                // 如果不存在，则创建一个新的FlowFile来承载错误信息
                StringWriter sw = new StringWriter();
                e.printStackTrace(new PrintWriter(sw));
                String exceptionAsString = sw.toString();
                FlowFile failureFlowFile = session.create();
                failureFlowFile = session.write(failureFlowFile, out -> out.write(exceptionAsString.getBytes(StandardCharsets.UTF_8)));
                session.transfer(failureFlowFile, REL_FAILURE);
            }
            context.yield();
        }
    }

    private ResultDataDto callJackyunApi(ProcessContext context, JSONObject bizData, ComponentLog logger) throws IOException, NoSuchAlgorithmException {
        String appKey = context.getProperty(PROP_APP_KEY).getValue();
        String appSecret = context.getProperty(PROP_APP_SECRET).getValue();
        String method = context.getProperty(PROP_API_METHOD).getValue();
        String version = context.getProperty(PROP_API_VERSION).getValue();

        SortedMap<String, String> sortedMap = new TreeMap<>();
        sortedMap.put("method", method);
        sortedMap.put("appkey", appKey);
        sortedMap.put("version", version);
        sortedMap.put("contenttype", "json");
        sortedMap.put("timestamp", DATETIME_FORMATTER.format(LocalDateTime.now()));
        sortedMap.put("bizcontent", bizData.toJSONString());

        StringBuilder sbSignData = new StringBuilder(appSecret);
        for (Map.Entry<String, String> entry : sortedMap.entrySet()) {
            sbSignData.append(entry.getKey()).append(entry.getValue());
        }
        sbSignData.append(appSecret);

        sortedMap.put("sign", md5Encrypt(sbSignData.toString().toLowerCase()));

        FormBody.Builder formBodyBuilder = new FormBody.Builder();
        for (Map.Entry<String, String> entry : sortedMap.entrySet()) {
            formBodyBuilder.add(entry.getKey(), entry.getValue());
        }
        RequestBody formBody = formBodyBuilder.build();

        Request request = new Request.Builder().url(context.getProperty(PROP_GATEWAY_URL).getValue()).post(formBody).build();

        long startTime = System.nanoTime(); // 使用纳秒计时更精确
        try (Response response = getHttpClient().newCall(request).execute()) {
            long endTime = System.nanoTime();
            long durationMillis = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);

            // 以 debug 级别记录日志
            logger.debug("吉客云API '{}' 调用耗时: {} ms", method, durationMillis);

            if (!response.isSuccessful()) {
                throw new IOException("非预期的HTTP响应码: " + response.code() + ", 响应体: " + response.body().string());
            }
            if (response.body() != null) {
                return JSON.parseObject(response.body().string(), ResultDataDto.class);
            }
            return null;
        } catch (IOException e) {
            long endTime = System.nanoTime();
            long durationMillis = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);
            logger.error("吉客云API '{}' 调用失败，耗时: {} ms。错误: {}", method, durationMillis, e.getMessage());
            throw e; // 将原始异常继续向上抛出
        }
    }

    private String md5Encrypt(String text) throws NoSuchAlgorithmException {
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        byte[] resultByte = text.getBytes(StandardCharsets.UTF_8);
        byte[] md5Bytes = md5.digest(resultByte);
        StringBuilder hexValue = new StringBuilder();
        for (byte md5Byte : md5Bytes) {
            int val = (md5Byte) & 0xff;
            if (val < 16) {
                hexValue.append("0");
            }
            hexValue.append(Integer.toHexString(val));
        }
        return hexValue.toString();
    }

    private boolean checkIsNull(JSONObject jsonObject) {
        if (jsonObject != null) {
            for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
                String key = entry.getKey();
                Object value = entry.getValue();
                if (value instanceof JSONArray) {
                    if (((JSONArray) value).size() > 0) {
                        return true;
                    }
                } else {
                    return true;
                }
            }
        }
        return false;
    }
}